/*******************************************************************************
 * Copyright (c) 2012-2016 Codenvy, S.A.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *   Codenvy, S.A. - initial API and implementation
 *******************************************************************************/
package org.eclipse.che.ide.maven.tools;

import org.eclipse.che.commons.xml.Element;
import org.eclipse.che.commons.xml.NewElement;

import java.util.HashMap;
import java.util.Map;

import static org.eclipse.che.commons.xml.NewElement.createElement;
import static org.eclipse.che.commons.xml.XMLTreeLocation.after;
import static org.eclipse.che.commons.xml.XMLTreeLocation.inTheBegin;
import static java.util.Collections.emptyMap;
import static java.util.Objects.requireNonNull;

/**
 * Describes <i>/project/build/plugins/plugin</i>.
 * <p/>
 * Supports next data:
 * <ul>
 * <li>artifactId</li>
 * <li>groupId</li>
 * <li>configuration</li>
 * </ul>
 *
 * @author Eugene Voevodin
 */
public class Plugin {

    private String              artifactId;
    private String              groupId;
    private Map<String, String> configuration;

    Element pluginElement;

    public Plugin() {
    }

    Plugin(Element element) {
        pluginElement = element;
        if (element.hasSingleChild("artifactId")) {
            artifactId = element.getChildText("artifactId");
        }
        if (element.hasSingleChild("groupId")) {
            groupId = element.getChildText("groupId");
        }
        if (element.hasSingleChild("configuration")) {
            configuration = fetchConfiguration(element.getSingleChild("configuration"));
        }
    }

    /**
     * Returns plugin artifact identifier
     */
    public String getArtifactId() {
        return artifactId;
    }

    /**
     * Returns plugin group identifier
     */
    public String getGroupId() {
        return groupId;
    }

    /**
     * Returns plugin configuration.
     * If plugin has nested configuration elements it will not be fetched.
     * <p/>
     * Consider following configuration:
     * <pre>
     * {@code
     *
     *     <configuration>
     *         <item1>value1</item1>
     *         <item2>value2</item2>
     *         <properties>
     *             <property1>property</property1>
     *         </properties>
     *     </configuration>
     * }
     * </pre>
     * <p/>
     * Resulting map will contain next data <i>item1="value1"</i>,
     * <i>item2="value2"</i> and <i>properties=null</i>
     * <p/>
     * <b>Note: update methods should not be used on returned map</b>
     *
     * @return plugin configuration or empty map when plugin doesn't have configuration
     */
    public Map<String, String> getConfiguration() {
        if (configuration == null) {
            return emptyMap();
        }
        return new HashMap<>(configuration);
    }

    /**
     * Sets plugin artifact identifier
     *
     * @param artifactId
     *         new artifact identifier, if new artifact id is {@code null} and current plugin element
     *         related with xml element then <i>artifactId</i> element will be removed from
     *         xml as well as from plugin model
     * @return this plugin instance
     */
    public Plugin setArtifactId(String artifactId) {
        this.artifactId = artifactId;
        if (!isNew()) {
            if (artifactId == null) {
                pluginElement.removeChild("artifactId");
            } else if (pluginElement.hasSingleChild("artifactId")) {
                pluginElement.getSingleChild("artifactId").setText(artifactId);
            } else {
                pluginElement.insertChild(createElement("artifactId", artifactId), after("groupId").or(inTheBegin()));
            }
        }
        return this;
    }

    /**
     * Sets plugin group identifier
     *
     * @param groupId
     *         new group identifier, if new group id is {@code null} and current plugin element
     *         related with xml element then <i>groupId</i> element will be removed from
     *         xml as well as from plugin model
     * @return this plugin instance
     */
    public Plugin setGroupId(String groupId) {
        this.groupId = groupId;
        if (!isNew()) {
            if (groupId == null) {
                pluginElement.removeChild("groupId");
            } else if (pluginElement.hasSingleChild("groupId")) {
                pluginElement.getSingleChild("groupId").setText(groupId);
            } else {
                pluginElement.insertChild(createElement("groupId", groupId), inTheBegin());
            }
        }
        return this;
    }

    /**
     * Sets new configuration with new configuration
     *
     * @param configuration
     *         new plugin configuration, if new configuration is {@code null} or <i>empty</i>
     *         and plugin element related with xml element then <i>configuration</i> element
     *         will be removed from xml as well as from plugin model
     * @return this plugin instance
     */
    public Plugin setConfiguration(Map<String, String> configuration) {
        if (configuration == null || configuration.isEmpty()) {
            removeConfiguration();
        } else {
            setConfiguration0(configuration);
        }
        return this;
    }

    /**
     * Sets configuration property value as {@literal <name>value</name>}.
     * <p/>
     * If element doesn't have configuration element it will be created as well.
     * <b>Note: it should not be used with nested configuration elements</b>
     *
     * @param name
     *         property name to set
     * @param value
     *         property value to set
     * @return this plugin instance
     * @throws NullPointerException
     *         when {@code name} or {@code value} is {@code null}
     */
    public Plugin setConfigProperty(String name, String value) {
        requireNonNull(name, "Configuration property name should not be null");
        requireNonNull(value, "Configuration property value should not be null");
        if (!isNew()) {
            addConfigPropertyToXML(name, value);
        }
        configuration().put(name, value);
        return this;
    }

    /**
     * Removes configuration property.
     * If configuration has nested element with removal {@code name}
     * it will be removed with all related children.
     *
     * @param name
     *         configuration property which indicated element that should be removed
     * @return this plugin instance
     * @throws NullPointerException
     *         when {@code name} is null
     */
    public Plugin removeConfigProperty(String name) {
        requireNonNull(name, "Configuration property name should ne null");
        if (configuration().remove(name) != null && !isNew()) {
            removeConfigPropertyFromXML(name);
        }
        return this;
    }

    /**
     * Returns plugin identifier as <i>groupId:artifactId</i>
     */
    public String getId() {
        return groupId + ':' + artifactId;
    }

    @Override
    public String toString() {
        return getId();
    }

    private void removeConfigPropertyFromXML(String name) {
        if (configuration.isEmpty()) {
            pluginElement.removeChild("configuration");
        } else {
            pluginElement.getSingleChild("configuration").removeChild(name);
        }
    }

    private void addConfigPropertyToXML(String name, String value) {
        if (configuration().containsKey(name)) {
            pluginElement.getSingleChild("configuration")
                         .getSingleChild(name)
                         .setText(value);
        } else if (configuration.isEmpty()) {
            pluginElement.appendChild(createElement("configuration", createElement(name, value)));
        } else {
            pluginElement.getSingleChild("configuration").appendChild(createElement(name, value));
        }
    }

    private Map<String, String> configuration() {
        return configuration == null ? configuration = new HashMap<>() : configuration;
    }

    private void setConfiguration0(Map<String, String> configuration) {
        this.configuration = new HashMap<>(configuration);

        if (isNew()) return;

        if (pluginElement.hasSingleChild("configuration")) {
            final Element confElement = pluginElement.getSingleChild("configuration");
            //remove all configuration properties from element
            for (Element property : confElement.getChildren()) {
                property.remove();
            }
            //append each new property to "configuration" element
            for (Map.Entry<String, String> property : configuration.entrySet()) {
                confElement.appendChild(createElement(property.getKey(), property.getValue()));
            }
        } else {
            final NewElement newConfiguration = createElement("configuration");
            for (Map.Entry<String, String> entry : configuration.entrySet()) {
                newConfiguration.appendChild(createElement(entry.getKey(), entry.getValue()));
            }
            //insert new configuration to xml
            pluginElement.appendChild(newConfiguration);
        }
    }

    private void removeConfiguration() {
        if (!isNew()) {
            pluginElement.removeChild("properties");
        }
        configuration = null;
    }

    private Map<String, String> fetchConfiguration(Element element) {
        final Map<String, String> configuration = new HashMap<>();
        for (Element configProperty : element.getChildren()) {
            if (!configProperty.hasChildren()) {
                configuration.put(configProperty.getName(), configProperty.getText());
            }
        }
        return configuration;
    }

    NewElement asXMLElement() {
        final NewElement xmlPlugin = createElement("plugin");
        if (groupId != null) {
            xmlPlugin.appendChild(createElement("groupId", groupId));
        }
        if (artifactId != null) {
            xmlPlugin.appendChild(createElement("artifactId", artifactId));
        }
        if (configuration != null && !configuration.isEmpty()) {
            final NewElement configElement = createElement("configuration");
            for (Map.Entry<String, String> configProperty : configuration.entrySet()) {
                configElement.appendChild(createElement(configProperty.getKey(), configProperty.getValue()));
            }
            xmlPlugin.appendChild(configElement);
        }
        return xmlPlugin;
    }

    private boolean isNew() {
        return pluginElement == null;
    }
}
